#!/bin/bash
#==============================================================#
# 脚本名     :   DMRT2AS
# 创建时间   :   2024-09-19 10:02:09
# 更新时间   :   2025-09-21 21:01:00
# 描述      :   达梦数据库一键切换实时备库和异步备库
# 路径      :   /soft/DMRT2AS
# 版本      :   1.0.0
# 作者      :   yuanzijian(yzj@dameng.com)
# Copyright (C) 2024-2025 Zijian Yuan
#==============================================================#
# 操作系统版本
os_version=7
# 达梦软件安装目录，默认 /dm
env_dm_home=/dm
# 定义数据库实例目录
data_path=
# 数据守护配置文件路径
dmwatcher_ini=
#定义业务IP数组
declare -a inst_hosts
# 定义实例名数组
declare -a inst_names
# 定义业务IP和实例名关联数组
declare -A hosts_inst_names
# 异步备库IP地址
as_public_ip=
# 定义异步备库IP地址数组
declare -a as_pubip_arr
# 数据守护切换模式，默认为自动切换
dw_mode=1
# 定义标签值，默认为0
os_flag_value=0
# 定义数据守护启动命令
dmwatcher_cmd=
# 定义实例启动
dm_cmd=
#==============================================================#
# 日志输出颜色
#==============================================================#
function color_printf() {
	declare -A color_map=(
		["red"]='\E[31m'
		["green"]='\E[1;32m'
		["yellow"]='\E[33m'
		["blue"]='\E[34m'
		["white"]='\E[37m'
	)
	local res='\E[0m'
	local color=${color_map[$1]:-"$2"}
	printf "${color}%-20s %-30s %-50s\n${res}" "$2" "$3" "$4"
}
#==============================================================#
# 帮助
#==============================================================#
function help() {
	# 打印参数
	print_options() {
		local options=("$@")
		# 调用 color_printf 函数，输出绿色字体
		# ${option%% *} 表示从 option 变量中删除最后一个空格及其后面的字符，保留前面的部分
		# ${option#* } 表示从 option 变量中删除第一个空格及其前面的字符，保留后面的部分
		for option in "${options[@]}"; do
			color_printf green "${option%% *}" "${option#* }"
		done
	}
	declare -a options=()
	printf "\n"
	color_printf blue "用途: 一键切换实时备库与异步备库"
	printf "\n"
	color_printf red "用法: DMRT2AS [选项] 选项值 { COMMAND | help }"
	printf "\n"
	options=(
		"-dm DW 切换模式，模式[1]：自动切换，模式[2]：手动切换，默认值：[1]"
		"-api DW 异步备库公网IP，以逗号隔开，例如：-api 192.168.2.28"
	)
	print_options "${options[@]}"
	printf "\n"
	color_printf red "注意：本脚本在使用之前，建议停止确认监视器，请谨慎使用！！！"
	printf "\n"
	exit 0
}
#==============================================================#
# 判断传进来的参数
#==============================================================#
function checkpara_null() {
	if [[ -z $2 || $2 == -* ]]; then
		printf "\n"
		color_printf red "请检查参数 [ $1 ] 的值是否为空！"
		printf "\n"
		exit 1
	fi
}
# 判断值是否包含
function checkpara_validity() {
	local option_name=$1
	local option_value=$2
	local valid_values=("${@:3}")
	if ! [[ " ${valid_values[*]} " =~ $option_value ]]; then
		printf "\n"
		color_printf red "请检查参数 [ $option_name ] 的值 \"$option_value\" 是否为 [ ${valid_values[*]} ] 中的一个！"
		printf "\n"
		exit 1
	fi
}
#==============================================================#
# 获取操作系统类型和版本
#==============================================================#
function defOsFlag() {
	# 定义 os_version
	libc_version=$(ldd --version | head -n 1 | awk '{print $NF}' | cut -d '.' -f 2)
	# 根据 libc_version 获取 os_version
	local -A os_version_map=(
		["6"]="12-16"
		["7"]="17-27"
		["8"]="28-33"
		["9"]="34-38"
		["10"]="39-99" # redhat10 是 ldd (GNU libc) 2.39
	)
	for value in "${!os_version_map[@]}"; do
		IFS="-" read -r start end <<<"${os_version_map[$value]}"
		if ((libc_version >= start && libc_version <= end)); then
			os_version="$value"
			break
		fi
	done
	# 校验是否匹配到 os_version
	if [[ -z "$os_version" ]]; then
		color_printf red "当前操作系统不在脚本支持列表中，如有需要请联系开发者适配！"
		exit 1
	fi
	#获取主机上所有IP地址，按空格分割IP地址，并逐一比较是否和传递的列表中第一个IP地址匹配
	IFS=' ' read -ra current_ips <<<"$(hostname -I)"
	# 遍历当前 IP 地址数组，并检查是否包含在过滤后的数组中
	for current_ip in "${current_ips[@]}"; do
		if [[ "${as_pubip_arr[*]}" =~ $current_ip ]]; then
			os_flag_value=1 # 如果当前 IP 包含在过滤后的数组中，更新标签值为 1
		fi
	done
}
#==============================================================#
# 获取数据库安装目录
#==============================================================#
function getDbInfo() {
	# 获取DM_HOME
	if [[ "$os_version" -ne 6 ]]; then
		env_dm_home=$(pgrep -af "dmap|dmserver" | awk '{print $2}' | xargs dirname | awk -F'/bin' '{print $1}' | head -1)
	else
		# shellcheck disable=SC2009
		env_dm_home=$(ps -ef | grep "dmap\|dmserver" | grep -v grep | awk '{split($8, a, "/bin"); print a[1]}' | head -1)
	fi
	# 导入数据库安装目录
	export DM_HOME="${env_dm_home}"
	export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DM_HOME/bin"
	# 定义数据守护启动命令
	dmwatcher_cmd=${env_dm_home}/bin/dmwatcher
	# 定义实例启动
	dm_cmd=${env_dm_home}/bin/dmserver
	# 获取DATA_PATH路径
	# 获取DM_HOME
	if [[ "$os_version" -ne 6 ]]; then
		data_path=$(pgrep -af "dmserver" | awk -F'=' '{print $2}' | xargs -I {} dirname {} | head -1)
	else
		# shellcheck disable=SC2009
		data_path=$(ps -ef | grep "dmserver" | grep -v grep | awk -F'=' '{print $2}' | xargs -I {} dirname {} | head -1)
	fi
	if [[ -z $data_path ]]; then
		printf "\n"
		color_printf red "注意：数据库没有正常启动，请检查数据库运行状态！！！"
		printf "\n"
		exit 1
	fi
	## 获取数据守护参数文件
	# shellcheck disable=SC2155
	# shellcheck disable=SC2009
	if [[ $os_version -ne 6 ]]; then
		dmwatcher_ini=$(pgrep -af "dmwatcher.ini" | awk '{split($3, a, "="); print a[2]}' | head -1)
	else
		dmwatcher_ini=$(ps -ef | grep "dmwatcher.ini" | grep -v grep | awk '{split($9, a, "="); print a[2]}' | head -1)
	fi
	if [[ -z $dmwatcher_ini ]]; then
		printf "\n"
		color_printf red "注意：数据守护没有正常启动，请检查数据守护运行状态！！！"
		printf "\n"
		exit 1
	fi
}
#==============================================================#
# 获取业务IP和实例名之间的关系
#==============================================================#
function getHostsAndNams() {
	# 备份并转换大写（保留原功能）
	sed -i.bak '/\//! s/.*/\U&/; s/MAL_TEMP_PATH/\U&/Ig' "${data_path}"/dmmal.ini
	# 逐行处理文件,将对应的值写入到数组里面
	while IFS= read -r line; do
		# 分割键值对（去除前后空格）
		IFS='=' read -r key value <<<"$line"
		key=$(echo "$key" | xargs)
		value=$(echo "$value" | xargs)
		case "$key" in
		MAL_INST_HOST)
			inst_hosts+=("$value")
			;;
		MAL_INST_NAME)
			inst_names+=("$value")
			;;
		esac
	done <"${data_path}"/dmmal.ini
	# 关联数组添加元素
	for ((i = 0; i < ${#inst_hosts[@]}; i++)); do
		hosts_inst_names["${inst_hosts[$i]}"]="${inst_names[$i]}"
	done
	# 判断获取到IP地址数组信息是否正确
	if [[ ${#inst_hosts[@]} -eq 0 ]]; then
		color_printf red "错误: 数组主机IP信息为空，请检查dmmal.ini文件中的参数：MAL_INST_HOST ！！！"
		exit 1
	fi
	# 判断异步备库IP是否传参正确
	if [[ -n $as_public_ip ]]; then
		for element in "${as_pubip_arr[@]}"; do
			if [[ ! "${inst_hosts[*]}" =~ $element ]]; then
				printf "\n"
				color_printf red "错误: 请检查 -api 参数值 $element 是否为集群节点IP！！！"
				printf "\n"
				exit 1
			fi
		done
	fi
	# 判断获取到实例名数组信息是否正确
	if [[ ${#inst_names[@]} -eq 0 ]]; then
		color_printf red "错误: 数组实例名信息为空，请检查dmmal.ini文件中的参数：MAL_INST_NAME ！！！"
		exit 1
	fi
	# 修改dmmal.ini属性
	chown dmdba:dinstall "${data_path}"/dmmal.ini
}
#==============================================================#
# 修改数据守护
#==============================================================#
function modWatcher() {
	# 备份数据守护配置文件
	sed -i.bak '/\//! s/.*/\U&/; s/\(INST_INI\|INST_STARTUP_CMD\)/\U&/Ig' "$dmwatcher_ini"
	# 定义数据守护模式
	local dwmode=""
	# 修改数据守护配置文件
	if [[ $dw_mode -eq 1 ]]; then
		dwmode=AUTO
	else
		dwmode=MANUAL
	fi
	# 数组inst_hosts元素个数减去as_pubip_arr等于1时，说明只有异步备库，没有实时备库
	a=$((${#inst_hosts[@]} - ${#as_pubip_arr[@]}))
	# 修改数据守护参数文件
	case $os_flag_value in
	0)
		if ((a == 1)); then
			sed -i '/DW_TYPE/Ic\DW_TYPE = LOCAL' "$dmwatcher_ini"
		else
			sed -i '/DW_TYPE/Ic\DW_TYPE = GLOBAL' "$dmwatcher_ini"
		fi
		sed -i "/DW_MODE/Ic\DW_MODE = $dwmode" "$dmwatcher_ini"
		;;
	1)
		sed -i '/DW_TYPE/Ic\DW_TYPE = LOCAL' "$dmwatcher_ini"
		sed -i '/DW_MODE/Ic\DW_MODE = MANUAL' "$dmwatcher_ini"
		;;
	esac
	# 修改 dmwatcher.ini 属性
	chown dmdba:dinstall "$dmwatcher_ini"
}
#==============================================================#
# 修改数据归档
#==============================================================#
function modArchive() {
	# 备份数据守护归档配置文件
	sed -i.bak '/\//! s/.*/\U&/; s/ARCH_DEST/\U&/Ig' "${data_path}"/dmarch.ini
	# 获取本地归档存储路径
	# shellcheck disable=SC2155
	local arch_dest_path=$(grep <"${data_path}"/dmarch.ini 'ARCH_DEST' | grep '/' | awk -F"=" '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
	# 获取本地归档日志文件大小
	# shellcheck disable=SC2155
	local arch_file_size=$(grep <"${data_path}"/dmarch.ini 'ARCH_FILE_SIZE' | awk -F"=" '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
	# 获取本地归档空间大小
	# shellcheck disable=SC2155
	local arch_space_limit=$(grep <"${data_path}"/dmarch.ini 'ARCH_SPACE_LIMIT' | awk -F"=" '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
	# 获取本地实例名
	# shellcheck disable=SC2155
	local instance_name=$(grep <"${data_path}"/dm.ini 'INSTANCE_NAME' | awk -F"[ =]+" '{print $2}')
	# 归档配置文件
	cat <<-EOF >"${data_path}"/dmarch.ini
ARCH_WAIT_APPLY = 1
[ARCHIVE_LOCAL1]
ARCH_TYPE            = LOCAL
ARCH_DEST            = $arch_dest_path
ARCH_FILE_SIZE       = $arch_file_size
ARCH_SPACE_LIMIT     = $arch_space_limit
EOF
	if [[ $os_flag_value -eq 0 ]]; then
		# 配置实[即]时主备归档
		for ((i = 0; i < ${#inst_hosts[@]}; i++)); do
			if [[ ${inst_names[$i]} == "$instance_name" || ${as_pubip_arr[*]} =~ ${inst_hosts[$i]} ]]; then
				continue
			fi
			cat <<-EOF >>"${data_path}"/dmarch.ini
[ARCHIVE_REALTIME$((i + 1))]
ARCH_TYPE = REALTIME
ARCH_DEST = ${inst_names[$i]}
EOF
		done
		# 配置同[异]步主备归档
		for ((i = 0; i < ${#as_pubip_arr[@]}; i++)); do
			cat <<-EOF >>"${data_path}"/dmarch.ini
[ARCHIVE_ASYNC$((i + 1))]
ARCH_TYPE            = ASYNC
ARCH_DEST            = ${hosts_inst_names[${as_pubip_arr[$i]}]}
ARCH_TIMER_NAME      = RT_TIMER$((i + 1))
ARCH_SEND_DELAY      = 0
EOF
		done
	fi
	# 修改 dmarch.ini 属性
	chown dmdba:dinstall "${data_path}"/dmarch.ini
}
#==============================================================#
# 修改定时器
#==============================================================#
function modTimer() {
	# 修改定时器是否开启
	case $os_flag_value in
	0)
		sed -i 's/TIMER_INI                       = 0/TIMER_INI                       = 1/g' "$data_path"/dm.ini
		;;
	1)
		sed -i 's/TIMER_INI                       = 1/TIMER_INI                       = 0/g' "$data_path"/dm.ini
		;;
	esac
	# 备份定时器
	[[ -f "${data_path}/dmtimer.ini" ]] && sed -i.bak 's/.*/\U&/' "${data_path}"/dmtimer.ini
	# 数组循环输出
	for ((i = 0; i < ${#as_pubip_arr[@]}; i++)); do
		if ((i == 0)); then
			# shellcheck disable=SC2188
			>"${data_path}/dmtimer.ini"
		fi
		cat <<-EOF >>"${data_path}"/dmtimer.ini
[RT_TIMER$((i + 1))]
TYPE = 10
FREQ_MONTH_WEEK_INTERVAL =
FREQ_SUB_INTERVAL =
#FREQ_MINUTE_INTERVAL = 1
REPEAT_INTERVAL= FREQ=SECONDLY;INTERVAL= 60
START_TIME = 01:00:00
END_TIME =
DURING_START_DATE = 2016-02-11 17:36:09
DURING_END_DATE = 9999-12-31 23:59:59
NO_END_DATE_FLAG = 1
DESCRIBE = RT_TIMER$((i + 1))
IS_VALID = 1
EOF
	done
}
#==============================================================#
# 判断命令是否执行成功
#==============================================================#
function run_command() {
	eval "$@" >/dev/null 2>&1  # 执行传入的命令和参数
	local status=$?            # 获取命令的退出状态
	if [ $status -eq 0 ]; then # 如果命令执行成功
		printf "命令 \"%s\" : \e[32m%s\e[0m。\n" "$*" "执行成功"
		return 0 # 返回 0，使得当前命令退出
	else
		printf "命令 \"%s\" : \e[31m%s\e[0m ，退出码为 %d。\n" "$*" "执行失败" "$status"
		return $status # 返回命令的退出状态，使得当前命令继续执行
	fi
}
#==============================================================#
# 获取数据库服务路径
#==============================================================#
function getDbServicePath() {
	if [[ $os_version -eq 6 ]]; then
		echo "/etc/rc.d/init.d"
	else
		echo "$env_dm_home/bin"
	fi
}
#==============================================================#
# 通用停止服务函数
# $1: 进程关键词（如 dmwatcher 或 dmserver）
# $2: 服务名通配符（如 DmWatcherService* 或 DmService*）
#==============================================================#
function stopService() {
	local cmd_keyword="$1"
	local service_pattern="$2"
	local wait_time=2
	# shellcheck disable=SC2155
	local db_service_path=$(getDbServicePath)
	if pgrep -f "${cmd_keyword}" &>/dev/null; then
		run_command "${db_service_path}/${service_pattern} stop"
		while pgrep -f "${cmd_keyword}" &>/dev/null; do
			sleep "$wait_time"
		done
	fi
}
#==============================================================#
# 停止数据库
#==============================================================#
function stopDb() {
	# 停止数据守护
	stopService "${dmwatcher_cmd}" "DmWatcherService*"
	# 停止数据库实例
	stopService "${dm_cmd}" "DmService*"
}
#==============================================================#
# 通用启动函数
# $1: 进程关键词(如 $dm_cmd 或 $dmwatcher_cmd)
# $2: 服务通配(如 DmService* 或 DmWatcherService*)
# $3: 是否 mount 模式(仅对实例有效)，为空表示非 mount
#==============================================================#
function startService() {
	local cmd_keyword=$1
	local svc_pattern=$2
	local mount_opt=$3
	# shellcheck disable=SC2155
	local db_service_path=$(getDbServicePath)
	local max_try=30
	local try_cnt=0
	while ! pgrep -f "${cmd_keyword}" &>/dev/null; do
		((try_cnt++))
		if [[ $max_try -gt 0 && $try_cnt -gt $max_try ]]; then
			printf "\n"
			color_printf red "ERROR: $(basename "${cmd_keyword}") 启动失败，已重试 $max_try 次" >&2
			printf "\n"
			return 1
		fi
		# 拼启动参数：mount 模式只针对实例
		if [[ -n $mount_opt ]]; then
			run_command "${db_service_path}/${svc_pattern} start mount"
		else
			run_command "${db_service_path}/${svc_pattern} start"
		fi
		sleep 10
	done
}
#==============================================================#
# 启动数据库
#==============================================================#
function startDb() {
	# 启动数据库实例
	startService "${dm_cmd}" "DmService*" "mount"
	# 启动数据守护
	startService "${dmwatcher_cmd}" "DmWatcherService*"
}
#==============================================================#
# 脚本入口
#==============================================================#
function main() {
	while [[ $1 ]]; do
		case $1 in
		-api | --as_public_ip)
			checkpara_null "$1" "$2"
			as_public_ip=$2
			IFS=',' read -ra as_pubip_arr <<<"$as_public_ip"
			shift 2
			;;
		-dm | --dw_mode)
			checkpara_null "$1" "$2"
			dw_mode=$2
			checkpara_validity "$1" "$dw_mode" "1" "2"
			shift 2
			;;
		-h | --help) help ;;
		*)
			printf "\n"
			color_printf red "脚本已退出安装，请检查是否参数是否正确，可以查看脚本帮助：'DMRT2AS --help' 获取更多信息！"
			printf "\n"
			exit 1
			;;
		esac
	done
	defOsFlag
	getDbInfo
	getHostsAndNams
	stopDb
	modWatcher
	modArchive
	modTimer
	startDb
}
main "$@"
